查看原文
其他

Google挖坑后人埋-ViewBinding(上)

徐宜生 群英传 2022-12-21

官网镇楼

https://developer.android.com/topic/libraries/view-binding

官方警告

Warning: The 'kotlin-android-extensions' Gradle plugin is deprecated. Please use this migration guide (https://goo.gle/kotlin-android-extensions-deprecation) to start working with View Binding (https://developer.android.com/topic/libraries/view-binding) and the 'kotlin-parcelize' plugin.

kotlin-android-extensions是个好东西,可以帮我们省掉很多需要写findViewById的场景。相信大部分的Kotlin开发者都在使用它进行Android开发,而且在之前的Android Studio创建Android项目时,都会自动帮你依赖:

apply plugin: 'kotlin-android-extensions'

但是现在你再创建Android项目,就不会自动帮你依赖了,其原因就是kotlin-android-extensions这个插件已经被废弃了。

为啥?Google这新技术迭代跟玩一样啊,有kotlin-android-extensions插件我不用,我就手写,哎,就是玩儿~

其实,kotlin-android-extensions插件还是有很多问题的,虽然它很方便,但实际上,这是牺牲掉一部分内存来换取的方便,在对应用性能日益严格的今天,这种做法势必会被淘汰掉。

kotlin-android-extensions三宗罪

内存问题

通过反编译kotlin-android-extensions的代码,你就会发现,通过kotlin-android-extensions,它会在代码中创建一个HashMap,用来存放所有的id和对应的View的缓存,如果缓存中没有需要的View,那么就通过findViewById去创建,否则就直接获取,这就是它的原理。

资源ID重名

由于kotlin-android-extensions是通过view的id名直接引用的,所以多个布局间的同名id,就需要手动对import进行重命名处理,而且经常会引用错误的布局文件,导致运行崩溃。

Kotlin only

只有Kotlin才可以使用。

当然,ViewBinding也不是银弹,对比kotlin-android-extensions,它也有一些问题:

  • 使用比kotlin-android-extensions复杂
  • 依然有需要手动处理的场景

当然也有一些优势:

  • Kotlin Java通吃
  • 空安全

ViewBinding初步

ViewBinding就是为了解决kotlin-android-extensions的这些使用问题而诞生的,它的目的只有一个,那就是避免重复的findViewById的同时,不影响应用性能。

要使用ViewBinding非常简单:

buildFeatures {
    viewBinding true
}

当我们开启ViewBinding之后,在编译时,AGP会自动帮我们给每个xml布局创建一个Binding类,Binding类的命名规则是将xml文件按驼峰方式重命名后,再加上Binding作为结尾得到的,例如splash_layout.xml会自动生成一个SplashLayoutBinding的类文件。

跨Module使用的时候,子Module也需要开启ViewBinding功能

这个Binding文件,实际上就相当于kotlin-android-extensions的HashMap,同时由于它在编译时就生成了,就不会占用运行时内存。

虽然这里生成了大量的XXXBinding文件,但是对编译速度的影响和生成Apk大小的影响几乎可以忽略:

  • 未使用的XXXBinding文件会在混淆时被删除
  • 编译器生成Binding文件的速度极快,同时是增加更新

ignore

如果你不想生成这个Binding类,可以通过下面的方式来过滤掉该文件的生成。

<FrameLayout
    xmlns:tools="http://schemas.android.com/tools"
    ...
    tools:viewBindingIgnore="true">
    ...
</FrameLayout>

使用

开启ViewBinding后,会给xml布局生成XXXBinding文件,位于build/generated/data_binding_base_class_source_out/目录下。

Activity

在Activity中使用ViewBinding一般需要使用到Binding类的inflate方法,一般使用方式如下所示。

private val binding by lazy { XxxBinding.inflate(layoutInflater) }

binding.TitleTextView.text = "Title"

Binding类还有一个getRoot方法,用来返回xml布局的根元素,所以setContentView(R.layout.xxxx)就可以替换为:

setContentView(binding.root)

Fragment

在Fragment中使用ViewBinding会比在Activity中使用要复杂一点,因为需要保证Binding类与Fragment的生命周期同步,示例代码如下所示。

class MainFragment : Fragment() {

    private var _binding: OutcircleMissionFansGroupBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        _binding = OutcircleMissionFansGroupBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

_binding和binding傻傻分不清吗?其实没什么区别,这是为了在Kotlin中将不可空类型置空的一种妥协方式,同样的代码逻辑,在Java中,就会非常简单了。

public class MainFragment extends Fragment {

    private FragmentMainBinding binding;

    @Override
    public View onCreateView(@NotNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        binding = FragmentMainBinding.inflate(inflater, container, false);
        return binding.getRoot();
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        binding = null;
    }
}

Adapter

除了Activity和Fragment,在Adapter中使用,特别是RecyclerView中使用,也是一个非常常见的使用场景。利用kotlin-android-extensions,我们可以借助LayoutContainer来在ViewHolder中直接使用View id,那么在ViewBinding中,使用方式就更简单了。

class DemoAdapter(val dataList: List<String>) : RecyclerView.Adapter<DemoAdapter.ViewHolder>() {

    inner class ViewHolder(binding: OutcircleMissionFansGroupBinding) : RecyclerView.ViewHolder(binding.root) {
        val title: TextView = binding.titleTextView
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val binding = OutcircleMissionFansGroupBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return ViewHolder(binding)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val data = dataList[position]
        holder.title.text = data.title
    }

    override fun getItemCount() = dataList.size
}

其实核心代码依然是inflate,套路都是一样的。

Dialog

原理依然是一个套路。

override fun onCreate(savedInstanceState: Bundle?) {
    binding = XXXXBinding.inflate(layoutInflater)
    setContentView(binding.root)
}

Include、Merge

在布局中通过include来引入新的布局也是一个很常用的方式,kotlin-android-extensions由于底层使用的是运行时findViewById,所以不会存在什么问题,但是ViewBinding就不一样了,由于它是编译时生成,所以需要指定id才可以使用。

因此,在ViewBinding中使用include的layout,有两种方式,一种是给include设置id,这样通过id就可以直接引用,代码如下所示。

<include 
        android:id="@+id/xxxxx"
        layout="@layout/xxxxxxx" />

这样使用的时候,代码如下:

XXXBinding.xxxInclude.xxxx

另外一种方式是直接使用新的Binding文件,因为所有的xml布局文件都会生成Binding,所以可以直接使用这个Binding文件。

IncludeXXXXXBinding.bind(binding.root).xxxxx

这种方式还可以解决Merge的引入问题。

迁移

更新一时爽,迁移火葬场。

目前还未找到现有项目从kotlin-android-extensions迁移到ViewBinding的好办法,如果当前的项目大量使用kotlin-android-extensions,那么迁移起来,就是一个巨大的工程,没有migration tools,也不能通过脚本更换,确实没找到什么好的办法。

向大家推荐下我的网站 https://xuyisheng.top/  点击原文一键直达

专注 Android-Kotlin-Flutter 欢迎大家访问


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存